"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = patch;

var _noop2 = _interopRequireDefault(require("lodash/noop"));

var _isFunction2 = _interopRequireDefault(require("lodash/isFunction"));

var _importHtmlEntry = require("import-html-entry");

var _singleSpa = require("single-spa");

var _apis = require("../../apis");

var _common = require("../common");

var css = _interopRequireWildcard(require("./css"));

/**
 * @author Kuitos
 * @since 2019-10-21
 */
var styledComponentSymbol = Symbol('styled-component-qiankun');
var attachElementContainerSymbol = Symbol('attach-proxy-container');
var rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
var rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild;
var rawBodyAppendChild = HTMLBodyElement.prototype.appendChild;
var rawBodyRemoveChild = HTMLBodyElement.prototype.removeChild;
var rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore;
var rawRemoveChild = HTMLElement.prototype.removeChild;
var rawDocumentCreateElement = Document.prototype.createElement;
var SCRIPT_TAG_NAME = 'SCRIPT';
var LINK_TAG_NAME = 'LINK';
var STYLE_TAG_NAME = 'STYLE';
var proxyContainerInfoMapper = new Map();

function isHijackingTag(tagName) {
  return (tagName === null || tagName === void 0 ? void 0 : tagName.toUpperCase()) === LINK_TAG_NAME || (tagName === null || tagName === void 0 ? void 0 : tagName.toUpperCase()) === STYLE_TAG_NAME || (tagName === null || tagName === void 0 ? void 0 : tagName.toUpperCase()) === SCRIPT_TAG_NAME;
}
/**
 * Check if a style element is a styled-component liked.
 * A styled-components liked element is which not have textContext but keep the rules in its styleSheet.cssRules.
 * Such as the style element generated by styled-components and emotion.
 * @param element
 */


function isStyledComponentsLike(element) {
  var _a, _b;

  return !element.textContent && (((_a = element.sheet) === null || _a === void 0 ? void 0 : _a.cssRules.length) || ((_b = getCachedRules(element)) === null || _b === void 0 ? void 0 : _b.length));
}

function getCachedRules(element) {
  return element[styledComponentSymbol];
}

function setCachedRules(element, cssRules) {
  Object.defineProperty(element, styledComponentSymbol, {
    value: cssRules,
    configurable: true,
    enumerable: false
  });
}

function patchCustomEvent(e, elementGetter) {
  Object.defineProperties(e, {
    srcElement: {
      get: elementGetter
    },
    target: {
      get: elementGetter
    }
  });
  return e;
}

function getOverwrittenAppendChildOrInsertBefore(opts) {
  return function appendChildOrInsertBefore(newChild, refChild) {
    var element = newChild;
    var rawDOMAppendOrInsertBefore = opts.rawDOMAppendOrInsertBefore;

    if (element.tagName) {
      // eslint-disable-next-line prefer-const
      var appName = opts.appName,
          appWrapperGetter = opts.appWrapperGetter,
          proxy = opts.proxy,
          singular = opts.singular,
          dynamicStyleSheetElements = opts.dynamicStyleSheetElements;
      var scopedCSS = opts.scopedCSS,
          excludeAssetFilter = opts.excludeAssetFilter;
      var storedContainerInfo = element[attachElementContainerSymbol];

      if (storedContainerInfo) {
        // eslint-disable-next-line prefer-destructuring
        appName = storedContainerInfo.appName; // eslint-disable-next-line prefer-destructuring

        singular = storedContainerInfo.singular; // eslint-disable-next-line prefer-destructuring

        appWrapperGetter = storedContainerInfo.appWrapperGetter; // eslint-disable-next-line prefer-destructuring

        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements; // eslint-disable-next-line prefer-destructuring

        proxy = storedContainerInfo.proxy;
      }

      var invokedByMicroApp = singular ? // check if the currently specified application is active
      // While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
      // but the url change listener must to wait until the current call stack is flushed.
      // This scenario may cause we record the stylesheet from react routing page dynamic injection,
      // and remove them after the url change triggered and qiankun app is unmouting
      // see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230
      (0, _singleSpa.checkActivityFunctions)(window.location).some(function (name) {
        return name === appName;
      }) : // have storedContainerInfo means it invoked by a micro app in multiply mode
      !!storedContainerInfo;

      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME:
          {
            var stylesheetElement = newChild;
            var href = stylesheetElement.href;

            if (!invokedByMicroApp || excludeAssetFilter && href && excludeAssetFilter(href)) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild);
            }

            var mountDOM = appWrapperGetter();

            if (scopedCSS) {
              css.process(mountDOM, stylesheetElement, appName);
            } // eslint-disable-next-line no-shadow


            dynamicStyleSheetElements.push(stylesheetElement);
            var referenceNode = mountDOM.contains(refChild) ? refChild : null;
            return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
          }

        case SCRIPT_TAG_NAME:
          {
            var _element = element,
                src = _element.src,
                text = _element.text; // some script like jsonp maybe not support cors which should't use execScripts

            if (!invokedByMicroApp || excludeAssetFilter && src && excludeAssetFilter(src)) {
              return rawDOMAppendOrInsertBefore.call(this, element, refChild);
            }

            var _mountDOM = appWrapperGetter();

            var fetch = _apis.frameworkConfiguration.fetch;

            var _referenceNode = _mountDOM.contains(refChild) ? refChild : null;

            if (src) {
              (0, _importHtmlEntry.execScripts)(null, [src], proxy, {
                fetch: fetch,
                strictGlobal: !singular,
                beforeExec: function beforeExec() {
                  Object.defineProperty(document, 'currentScript', {
                    get: function get() {
                      return element;
                    },
                    configurable: true
                  });
                },
                success: function success() {
                  // we need to invoke the onload event manually to notify the event listener that the script was completed
                  // here are the two typical ways of dynamic script loading
                  // 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138
                  // 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64
                  var loadEvent = new CustomEvent('load');

                  if ((0, _isFunction2.default)(element.onload)) {
                    element.onload(patchCustomEvent(loadEvent, function () {
                      return element;
                    }));
                  } else {
                    element.dispatchEvent(loadEvent);
                  }

                  element = null;
                },
                error: function error() {
                  var errorEvent = new CustomEvent('error');

                  if ((0, _isFunction2.default)(element.onerror)) {
                    element.onerror(patchCustomEvent(errorEvent, function () {
                      return element;
                    }));
                  } else {
                    element.dispatchEvent(errorEvent);
                  }

                  element = null;
                }
              });
              var dynamicScriptCommentElement = document.createComment("dynamic script ".concat(src, " replaced by qiankun"));
              return rawDOMAppendOrInsertBefore.call(_mountDOM, dynamicScriptCommentElement, _referenceNode);
            }

            (0, _importHtmlEntry.execScripts)(null, ["<script>".concat(text, "</script>")], proxy, {
              strictGlobal: !singular,
              success: element.onload,
              error: element.onerror
            });
            var dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
            return rawDOMAppendOrInsertBefore.call(_mountDOM, dynamicInlineScriptCommentElement, _referenceNode);
          }

        default:
          break;
      }
    }

    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  };
}

function getNewRemoveChild(opts) {
  return function removeChild(child) {
    var headOrBodyRemoveChild = opts.headOrBodyRemoveChild;

    try {
      var tagName = child.tagName;

      if (isHijackingTag(tagName)) {
        var appWrapperGetter = opts.appWrapperGetter;
        var storedContainerInfo = child[attachElementContainerSymbol];

        if (storedContainerInfo) {
          // eslint-disable-next-line prefer-destructuring
          appWrapperGetter = storedContainerInfo.appWrapperGetter;
        } // container may had been removed while app unmounting if the removeChild action was async


        var container = appWrapperGetter();

        if (container.contains(child)) {
          return rawRemoveChild.call(container, child);
        }
      }
    } catch (e) {
      console.warn(e);
    }

    return headOrBodyRemoveChild.call(this, child);
  };
}

function patchHTMLDynamicAppendPrototypeFunctions(appName, appWrapperGetter, proxy) {
  var singular = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  var scopedCSS = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
  var dynamicStyleSheetElements = arguments.length > 5 ? arguments[5] : undefined;
  var excludeAssetFilter = arguments.length > 6 ? arguments[6] : undefined;

  // Just overwrite it while it have not been overwrite
  if (HTMLHeadElement.prototype.appendChild === rawHeadAppendChild && HTMLBodyElement.prototype.appendChild === rawBodyAppendChild && HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore) {
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      appName: appName,
      appWrapperGetter: appWrapperGetter,
      proxy: proxy,
      singular: singular,
      dynamicStyleSheetElements: dynamicStyleSheetElements,
      scopedCSS: scopedCSS,
      excludeAssetFilter: excludeAssetFilter
    });
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      appName: appName,
      appWrapperGetter: appWrapperGetter,
      proxy: proxy,
      singular: singular,
      dynamicStyleSheetElements: dynamicStyleSheetElements,
      scopedCSS: scopedCSS,
      excludeAssetFilter: excludeAssetFilter
    });
    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore,
      appName: appName,
      appWrapperGetter: appWrapperGetter,
      proxy: proxy,
      singular: singular,
      dynamicStyleSheetElements: dynamicStyleSheetElements,
      scopedCSS: scopedCSS,
      excludeAssetFilter: excludeAssetFilter
    });
  } // Just overwrite it while it have not been overwrite


  if (HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter: appWrapperGetter,
      headOrBodyRemoveChild: rawHeadRemoveChild
    });
    HTMLBodyElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter: appWrapperGetter,
      headOrBodyRemoveChild: rawBodyRemoveChild
    });
  }

  return function unpatch(recoverPrototype) {
    if (recoverPrototype) {
      HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
      HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
      HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
      HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;
      HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
    }
  };
}

function patchDocumentCreateElement(appName, appWrapperGetter, singular, proxy, dynamicStyleSheetElements) {
  if (singular) {
    return _noop2.default;
  }

  proxyContainerInfoMapper.set(proxy, {
    appName: appName,
    proxy: proxy,
    appWrapperGetter: appWrapperGetter,
    dynamicStyleSheetElements: dynamicStyleSheetElements,
    singular: singular
  });

  if (Document.prototype.createElement === rawDocumentCreateElement) {
    Document.prototype.createElement = function createElement(tagName, options) {
      var element = rawDocumentCreateElement.call(this, tagName, options);

      if (isHijackingTag(tagName)) {
        var proxyContainerInfo = proxyContainerInfoMapper.get(this[_common.attachDocProxySymbol]);

        if (proxyContainerInfo) {
          Object.defineProperty(element, attachElementContainerSymbol, {
            value: proxyContainerInfo,
            enumerable: false
          });
        }
      }

      return element;
    };
  }

  return function unpatch(recoverPrototype) {
    proxyContainerInfoMapper.delete(proxy);

    if (recoverPrototype) {
      Document.prototype.createElement = rawDocumentCreateElement;
    }
  };
}

var bootstrappingPatchCount = 0;
var mountingPatchCount = 0;
/**
 * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
 * Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container),
 * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list.
 * @param appName
 * @param appWrapperGetter
 * @param proxy
 * @param mounting
 * @param singular
 * @param scopedCSS
 * @param excludeAssetFilter
 */

function patch(appName, appWrapperGetter, proxy) {
  var mounting = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
  var singular = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
  var scopedCSS = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
  var excludeAssetFilter = arguments.length > 6 ? arguments[6] : undefined;
  var dynamicStyleSheetElements = [];
  var unpatchDocumentCreate = patchDocumentCreateElement(appName, appWrapperGetter, singular, proxy, dynamicStyleSheetElements);
  var unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(appName, appWrapperGetter, proxy, singular, scopedCSS, dynamicStyleSheetElements, excludeAssetFilter);
  if (!mounting) bootstrappingPatchCount++;
  if (mounting) mountingPatchCount++;
  return function free() {
    // bootstrap patch just called once but its freer will be called multiple times
    if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--;
    if (mounting) mountingPatchCount--;
    var allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; // release the overwrite prototype after all the micro apps unmounted

    unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
    unpatchDocumentCreate(allMicroAppUnmounted);
    dynamicStyleSheetElements.forEach(function (stylesheetElement) {
      /*
         With a styled-components generated style element, we need to record its cssRules for restore next re-mounting time.
         We're doing this because the sheet of style element is going to be cleaned automatically by browser after the style element dom removed from document.
         see https://www.w3.org/TR/cssom-1/#associated-css-style-sheet
         */
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        if (stylesheetElement.sheet) {
          // record the original css rules of the style element for restore
          setCachedRules(stylesheetElement, stylesheetElement.sheet.cssRules);
        }
      } // As now the sub app content all wrapped with a special id container,
      // the dynamic style sheet would be removed automatically while unmoutting

    });
    return function rebuild() {
      dynamicStyleSheetElements.forEach(function (stylesheetElement) {
        // re-append the dynamic stylesheet to sub-app container
        // Using document.head.appendChild ensures that appendChild calls
        // can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);
        /*
        get the stored css rules from styled-components generated element, and the re-insert rules for them.
        note that we must do this after style element had been added to document, which stylesheet would be associated to the document automatically.
        check the spec https://www.w3.org/TR/cssom-1/#associated-css-style-sheet
         */

        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          var cssRules = getCachedRules(stylesheetElement);

          if (cssRules) {
            // eslint-disable-next-line no-plusplus
            for (var i = 0; i < cssRules.length; i++) {
              var cssRule = cssRules[i];
              stylesheetElement.sheet.insertRule(cssRule.cssText);
            }
          }
        }
      }); // As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding

      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}